﻿using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;

namespace FlexField.Metadata.TestHarness
{
    public abstract class MetadataManagerTestBase
    {
        /// <summary>
        /// Error describer.
        /// </summary>
        protected readonly MetadataErrorDescriber Errors = new();

        /// <summary>
        /// Creates the context object for a test, typically a DbContext.
        /// </summary>
        /// <returns>The context object for a test, typically a DbContext.</returns>
        protected abstract object CreateDbContext();

        /// <summary>
        /// Adds an ITypeStore to services for the test.
        /// </summary>
        /// <param name="builder">The <see cref="MetadataBuilder"/>.</param>
        /// <param name="context">The context for the store to use, optional.</param>
        protected abstract void AddTypeSore(MetadataBuilder builder, object context = null);

        /// <summary>
        /// Adds an IFieldStore to services for the test.
        /// </summary>
        /// <param name="builder">The <see cref="MetadataBuilder"/>.</param>
        /// <param name="context">The context for the store to use, optional.</param>
        protected abstract void AddFieldStore(MetadataBuilder builder, object context = null);

        protected virtual MetadataBuilder SetupBuilder(IServiceCollection services, object context)
        {
            var builder = services.AddMetadataCore<Guid>();
            AddTypeSore(builder, context);
            AddFieldStore(builder, context);
            services.AddSingleton<ILogger<MetadataManager<Guid>>>(new TestLogger<MetadataManager<Guid>>());
            return builder;
        }

        /// <summary>
        /// Configure the service collection used for tests.
        /// </summary>
        /// <param name="services"></param>
        /// <param name="context"></param>
        protected virtual void SetupServices(IServiceCollection services, object context)
            => SetupBuilder(services, context);

        /// <summary>
        /// Creates the metadata manager used for tests.
        /// </summary>
        /// <param name="context">The context that will be passed into the store, typically a db context.</param>
        /// <param name="services">The service collection to use, optional.</param>
        /// <param name="configureServices">Delegate used to configure the services, optional.</param>
        /// <returns>The metadata manager to use for tests.</returns>
        protected virtual MetadataManager<Guid> CreateManager(object context = null, IServiceCollection services = null,
            Action<IServiceCollection> configureServices = null)
        {
            services ??= new ServiceCollection();
            context ??= CreateDbContext();
            SetupServices(services, context);
            configureServices?.Invoke(services);
            return services.BuildServiceProvider().GetService<MetadataManager<Guid>>();
        }

        protected virtual MetadataEntity<Guid> CreateTestType(string typeName = "", string displayName = "")
        {
            return new MetadataEntity<Guid>
            {
                EntityName = typeName,
                DisplayName = displayName,
                Id = Guid.NewGuid()
            };
        }

        protected virtual MetadataField<Guid> CreateTestField(string fieldName = "", string displayName = "", string fieldType = "")
        {
            return new MetadataField<Guid>
            {
                FieldName = fieldName,
                DisplayName = displayName,
                ValueType = fieldType,
                Id = Guid.NewGuid()
            };
        }

        [Fact]
        public async Task CanCreateType()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
        }

        [Fact]
        public async Task CanCreateField()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var field = CreateTestField(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "string");
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));
        }

        [Fact]
        public async Task CanDeleteType()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            MetadataResultAssert.IsSuccess(await manager.DeleteTypeAsync(type));
            Assert.Null(await manager.FindTypeByIdAsync(type.Id));
        }

        [Fact]
        public async Task CanDeleteField()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var field = CreateTestField(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "string");
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));
            MetadataResultAssert.IsSuccess(await manager.DeleteFieldAsync(type.Id, field));
            Assert.Null(await manager.FindFieldByIdAsync(field.Id));
        }

        [Fact]
        public async Task CanUpdateTypeName()
        {
            var manager = CreateManager();
            var oldName = Guid.NewGuid().ToString();
            var type = CreateTestType(oldName, Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var newName = Guid.NewGuid().ToString();
            type.EntityName = newName;
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));
            Assert.Null(await manager.FindTypeByNameAsync(oldName));
            Assert.NotNull(await manager.FindTypeByNameAsync(newName));
        }

        [Fact]
        public async Task CanUpdateFieldName()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var oldName = Guid.NewGuid().ToString();
            var field = CreateTestField(oldName, Guid.NewGuid().ToString(), "string");
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));
            var newName = Guid.NewGuid().ToString();
            field.FieldName = newName;
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));
            Assert.Null(await manager.FindFieldByNameAsync(type.Id, oldName));
            Assert.NotNull(await manager.FindFieldByNameAsync(type.Id, newName));
        }

        [Fact]
        public async Task CanUpdateTypeDisplayName()
        {
            var manager = CreateManager();
            var oldName = Guid.NewGuid().ToString();
            var type = CreateTestType(Guid.NewGuid().ToString(), oldName);
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var newName = Guid.NewGuid().ToString();
            type.DisplayName = newName;
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));

            var typeId = type.Id;
            type = await manager.FindTypeByIdAsync(typeId);
            Assert.NotNull(type);
            Assert.Equal(newName, type.DisplayName);
        }

        [Fact]
        public async Task CanUpdateFieldDisplayName()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));

            var oldName = Guid.NewGuid().ToString();
            var field = CreateTestField(Guid.NewGuid().ToString(), oldName);
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));

            var newName = Guid.NewGuid().ToString();
            field.DisplayName = newName;
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));

            var fieldId = field.Id;
            field = await manager.FindFieldByIdAsync(fieldId);
            Assert.NotNull(field);
            Assert.Equal(newName, field.DisplayName);
        }

        [Fact]
        public async Task CanUpdateTypeContinuous()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            type.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));

            type.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));
        }

        [Fact]
        public async Task CanUpdateFieldContinuous()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));

            var field = CreateTestField(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));

            field.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));

            field.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));
        }

        [Fact]
        public async Task UpdateTypeConcurrencyCheck()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));

            var otherType = await manager.FindTypeByIdAsync(type.Id);
            otherType.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(otherType));

            type.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsFailure(await manager.UpdateTypeAsync(type), Errors.ConcurrencyFailure());
        }

        [Fact]
        public async Task UpdateFieldConcurrencyCheck()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));

            var field = CreateTestField(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));

            var otherField = await manager.FindFieldByIdAsync(field.Id);
            otherField.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, otherField));

            field.DisplayName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsFailure(await manager.UpdateFieldAsync(type.Id, field), Errors.ConcurrencyFailure());
        }

        [Fact]
        public async Task ValidateTypeCheckTypeName()
        {
            var manager = CreateManager();
            var typeName = "UpdateAsync" + Guid.NewGuid();
            var newTypeName = "New" + Guid.NewGuid();
            var type = CreateTestType(typeName);
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            Assert.Null(await manager.FindTypeByNameAsync(newTypeName));
            type.EntityName = newTypeName;
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));
            Assert.NotNull(await manager.FindTypeByNameAsync(newTypeName));
            Assert.Null(await manager.FindTypeByNameAsync(typeName));

            var newType = CreateTestType(typeName);
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(newType));
            var error = Errors.InvalidTypeName("");
            newType.EntityName = "";
            MetadataResultAssert.IsFailure(await manager.UpdateTypeAsync(newType), error);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Type validation failed: {error.Code}.");

            error = Errors.DuplicateTypeName(newTypeName);
            newType.EntityName = newTypeName;
            MetadataResultAssert.IsFailure(await manager.UpdateTypeAsync(newType), error);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Type validation failed: {error.Code}.");
        }

        [Fact]
        public async Task ValidateFieldCheckFieldName()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString(), Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var fieldName = "UpdateAsync" + Guid.NewGuid();
            var newFieldName = "New" + Guid.NewGuid();

            var field = CreateTestField(fieldName);
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));
            Assert.Null(await manager.FindFieldByNameAsync(type.Id, newFieldName));
            field.FieldName = newFieldName;
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));
            Assert.NotNull(await manager.FindFieldByNameAsync(type.Id, newFieldName));
            Assert.Null(await manager.FindFieldByNameAsync(type.Id, fieldName));

            var newField = CreateTestField(fieldName);
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, newField));
            var error = Errors.InvalidFieldName("");
            newField.FieldName = "";
            MetadataResultAssert.IsFailure(await manager.UpdateFieldAsync(type.Id, newField), error);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Field validation failed: {error.Code}.");

            error = Errors.DuplicateFieldName(newFieldName);
            newField.FieldName = newFieldName;
            MetadataResultAssert.IsFailure(await manager.UpdateFieldAsync(type.Id, newField), error);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Field validation failed: {error.Code}.");
        }

        [Fact]
        public async Task CanFindTypeById()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            Assert.NotNull(await manager.FindTypeByIdAsync(type.Id));
        }

        [Fact]
        public async Task CanFindFieldById()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var field = CreateTestField(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));
            Assert.NotNull(await manager.FindFieldByIdAsync(field.Id));
        }

        [Fact]
        public async Task CanFindTypeByName()
        {
            var manager = CreateManager();
            var typeName = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(CreateTestType(typeName)));
            Assert.NotNull(await manager.FindTypeByNameAsync(typeName));
        }

        [Fact]
        public async Task CanFindFieldByName()
        {
            var manager = CreateManager();
            var fieldName = Guid.NewGuid().ToString();
            var type1 = CreateTestType(Guid.NewGuid().ToString());
            var type2 = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type1));
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type2));
            var field1 = CreateTestField(fieldName);
            var field2 = CreateTestField(fieldName);
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type1.Id, field1));
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type2.Id, field2));

            var field = await manager.FindFieldByNameAsync(type1.Id, fieldName);
            Assert.NotNull(field);
            Assert.Equal(field1.Id, field.Id);

            field = await manager.FindFieldByNameAsync(type2.Id, fieldName);
            Assert.NotNull(field);
            Assert.Equal(field2.Id, field.Id);
        }

        [Fact]
        public async Task TypeValidatorCanBlockCreate()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            manager.EntityValidators.Clear();
            manager.EntityValidators.Add(new AlwaysBadValidator<Guid>());
            MetadataResultAssert.IsFailure(await manager.CreateEntityAsync(type), AlwaysBadValidator<Guid>.ErrorMessage);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Type validation failed: {AlwaysBadValidator<Guid>.ErrorMessage.Code}.");
        }

        [Fact]
        public async Task FieldValidatorCanBlockCreate()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));

            var user = CreateTestField(Guid.NewGuid().ToString());
            manager.FieldValidators.Clear();
            manager.FieldValidators.Add(new AlwaysBadValidator<Guid>());
            MetadataResultAssert.IsFailure(await manager.CreateFieldAsync(type.Id, user), AlwaysBadValidator<Guid>.ErrorMessage);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Field validation failed: {AlwaysBadValidator<Guid>.ErrorMessage.Code}.");
        }

        [Fact]
        public async Task TypeValidatorCanBlockUpdate()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            manager.EntityValidators.Clear();
            manager.EntityValidators.Add(new AlwaysBadValidator<Guid>());
            MetadataResultAssert.IsFailure(await manager.UpdateTypeAsync(type), AlwaysBadValidator<Guid>.ErrorMessage);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Type validation failed: {AlwaysBadValidator<Guid>.ErrorMessage.Code}.");
        }

        [Fact]
        public async Task FieldValidatorCanBlockUpload()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var field = CreateTestField(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));
            manager.FieldValidators.Clear();
            manager.FieldValidators.Add(new AlwaysBadValidator<Guid>());
            MetadataResultAssert.IsFailure(await manager.UpdateFieldAsync(type.Id, field), AlwaysBadValidator<Guid>.ErrorMessage);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Field validation failed: {AlwaysBadValidator<Guid>.ErrorMessage.Code}.");
        }

        [Fact]
        public async Task CanChainTypeValidators()
        {
            var manager = CreateManager();
            manager.EntityValidators.Clear();
            var type = CreateTestType(Guid.NewGuid().ToString());
            manager.EntityValidators.Add(new AlwaysBadValidator<Guid>());
            manager.EntityValidators.Add(new AlwaysBadValidator<Guid>());
            var result = await manager.CreateEntityAsync(type);
            MetadataResultAssert.IsFailure(result, AlwaysBadValidator<Guid>.ErrorMessage);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Type validation failed: {AlwaysBadValidator<Guid>.ErrorMessage.Code};{AlwaysBadValidator<Guid>.ErrorMessage.Code}.");
            Assert.Equal(2, result.Errors.Count());
        }

        [Fact]
        public async Task CanChainFieldValidators()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var field = CreateTestField(Guid.NewGuid().ToString());
            manager.FieldValidators.Clear();
            manager.FieldValidators.Add(new AlwaysBadValidator<Guid>());
            manager.FieldValidators.Add(new AlwaysBadValidator<Guid>());
            var result = await manager.CreateFieldAsync(type.Id, field);
            MetadataResultAssert.IsFailure(result, AlwaysBadValidator<Guid>.ErrorMessage);
            MetadataResultAssert.VerifyLogMessage(manager.Logger, $"Field validation failed: {AlwaysBadValidator<Guid>.ErrorMessage.Code};{AlwaysBadValidator<Guid>.ErrorMessage.Code}.");
            Assert.Equal(2, result.Errors.Count());
        }

        [Fact]
        public async Task CanCreateTypeWithProperties()
        {
            var manager = CreateManager();
            var propertyName = Guid.NewGuid().ToString();
            var propertyValue = Guid.NewGuid().ToString();
            var type = CreateTestType(Guid.NewGuid().ToString());
            type.Properties[propertyName] = propertyValue;
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var typeId = type.Id;
            type = await manager.FindTypeByIdAsync(typeId);
            Assert.NotNull(type);
            Assert.Equal(1, type.Properties.Count);
            Assert.True(type.Properties.TryGetValue(propertyName, out var propValue));
            Assert.Equal(propertyValue, propValue);
        }

        [Fact]
        public async Task CanUpdateTypeChangeProperties()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            type.Properties[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var typeId = type.Id;

            type = await manager.FindTypeByIdAsync(typeId);
            var propertyName = Guid.NewGuid().ToString();
            var propertyValue = Guid.NewGuid().ToString();
            type.Properties[propertyName] = propertyValue;
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));

            type = await manager.FindTypeByIdAsync(typeId);
            Assert.NotNull(type);
            Assert.Equal(2, type.Properties.Count);
            Assert.True(type.Properties.TryGetValue(propertyName, out var propValue));
            Assert.Equal(propertyValue, propValue);

            var newPropertyValue = Guid.NewGuid().ToString();
            type.Properties[propertyName] = newPropertyValue;
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));

            type = await manager.FindTypeByIdAsync(typeId);
            Assert.NotNull(type);
            Assert.Equal(2, type.Properties.Count);
            Assert.True(type.Properties.TryGetValue(propertyName, out propValue));
            Assert.Equal(newPropertyValue, propValue);

            Assert.True(type.Properties.Remove(propertyName));
            MetadataResultAssert.IsSuccess(await manager.UpdateTypeAsync(type));
            type = await manager.FindTypeByIdAsync(typeId);
            Assert.NotNull(type);
            Assert.Equal(1, type.Properties.Count);
            Assert.False(type.Properties.ContainsKey(propertyName));
        }

        [Fact]
        public async Task CanCreateFieldWithProperties()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var propertyName = Guid.NewGuid().ToString();
            var propertyValue = Guid.NewGuid().ToString();
            var field = CreateTestField(Guid.NewGuid().ToString());
            field.Properties[propertyName] = propertyValue;
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));

            var fieldId = field.Id;
            field = await manager.FindFieldByIdAsync(fieldId);
            Assert.NotNull(field);
            Assert.Equal(1, field.Properties.Count);
            Assert.True(field.Properties.TryGetValue(propertyName, out var propValue));
            Assert.Equal(propertyValue, propValue);
        }

        [Fact]
        public async Task CanUpdateFieldChangeProperties()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var field = CreateTestField(Guid.NewGuid().ToString());
            field.Properties[Guid.NewGuid().ToString()] = Guid.NewGuid().ToString();
            MetadataResultAssert.IsSuccess(await manager.CreateFieldAsync(type.Id, field));

            var fieldId = field.Id;
            field = await manager.FindFieldByIdAsync(fieldId);
            var propertyName = Guid.NewGuid().ToString();
            var propertyValue = Guid.NewGuid().ToString();
            field.Properties[propertyName] = propertyValue;
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));

            field = await manager.FindFieldByIdAsync(fieldId);
            Assert.NotNull(field);
            Assert.Equal(2, field.Properties.Count);
            Assert.True(field.Properties.TryGetValue(propertyName, out var propValue));
            Assert.Equal(propertyValue, propValue);

            var newPropertyValue = Guid.NewGuid().ToString();
            field.Properties[propertyName] = newPropertyValue;
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));

            field = await manager.FindFieldByIdAsync(fieldId);
            Assert.NotNull(field);
            Assert.Equal(2, field.Properties.Count);
            Assert.True(field.Properties.TryGetValue(propertyName, out propValue));
            Assert.Equal(newPropertyValue, propValue);

            Assert.True(field.Properties.Remove(propertyName));
            MetadataResultAssert.IsSuccess(await manager.UpdateFieldAsync(type.Id, field));
            field = await manager.FindFieldByIdAsync(fieldId);
            Assert.NotNull(field);
            Assert.Equal(1, field.Properties.Count);
            Assert.False(field.Properties.ContainsKey(propertyName));
        }

        [Fact]
        public async Task CanFindTypeWithProperty()
        {
            var manager = CreateManager();
            for (int i = 0; i < 6; i++)
            {
                var type = CreateTestType(Guid.NewGuid().ToString());
                if (i % 2 == 0)
                {
                    type.Properties["foo"] = "bar";
                }

                await manager.CreateEntityAsync(type);
            }

            Assert.Equal(3, (await manager.FindTypesByPropertyAsync("foo", "bar")).Count);
            Assert.Equal(0, (await manager.FindTypesByPropertyAsync("123", "456")).Count);
        }

        [Fact]
        public async Task CanFindFieldWithProperty()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var otherType = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(otherType));
            for (int i = 0; i < 6; i++)
            {
                var fieldName = Guid.NewGuid().ToString();
                var field = CreateTestField(fieldName);
                var otherField = CreateTestField(fieldName);
                if (i % 2 == 0)
                {
                    field.Properties["foo"] = "bar";
                    otherField.Properties["foo"] = "bar";
                }
                await manager.CreateFieldAsync(type.Id, field);
                await manager.CreateFieldAsync(otherType.Id, otherField);
            }

            var fields = await manager.FindFieldsByPropertyAsync(type.Id, "foo", "bar");
            Assert.Equal(3, fields.Count);
            Assert.Equal("bar", fields[0].Properties["foo"]);
            Assert.Equal(0, (await manager.FindFieldsByPropertyAsync(type.Id, "123", "456")).Count);

            fields = await manager.FindFieldsByPropertyAsync(otherType.Id, "foo", "bar");
            Assert.Equal(3, fields.Count);
            Assert.Equal("bar", fields[0].Properties["foo"]);
            Assert.Equal(0, (await manager.FindFieldsByPropertyAsync(otherType.Id, "123", "456")).Count);
        }

        [Fact]
        public async Task CanGetFieldsInType()
        {
            var manager = CreateManager();
            var type = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(type));
            var otherType = CreateTestType(Guid.NewGuid().ToString());
            MetadataResultAssert.IsSuccess(await manager.CreateEntityAsync(otherType));
            for (int i = 0; i < 6; i++)
            {
                var fieldName = Guid.NewGuid().ToString();
                var field = CreateTestField(fieldName);
                field.Properties["foo"] = "bar" + type.EntityName;
                await manager.CreateFieldAsync(type.Id, field);

                if (i % 2 == 0)
                {
                    var otherField = CreateTestField(fieldName);
                    otherField.Properties["foo"] = "bar" + otherType.EntityName;
                    await manager.CreateFieldAsync(otherType.Id, otherField);
                }
            }
            var fields = await manager.GetFieldsInTypeAsync(type.Id);
            Assert.Equal(6, fields.Count);
            Assert.Equal("bar" + type.EntityName, fields[0].Properties["foo"]);

            fields = await manager.GetFieldsInTypeAsync(otherType.Id);
            Assert.Equal(3, fields.Count);
            Assert.Equal("bar" + otherType.EntityName, fields[0].Properties["foo"]);
        }

        [Fact]
        public async Task ThrowWhenDisposedTest()
        {
            var manager = CreateManager();
            manager.Dispose();

            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.CreateEntityAsync(null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.UpdateTypeAsync(null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.DeleteTypeAsync(null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.FindTypeByIdAsync(Guid.NewGuid()));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.FindTypeByNameAsync(null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.FindTypesByPropertyAsync(null, null));

            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.CreateFieldAsync(Guid.NewGuid(), null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.UpdateFieldAsync(Guid.NewGuid(), null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.DeleteFieldAsync(Guid.NewGuid(), null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.FindFieldByIdAsync(Guid.NewGuid()));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.FindFieldByNameAsync(Guid.NewGuid(), null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.FindFieldsByPropertyAsync(Guid.NewGuid(), null, null));
            await Assert.ThrowsAsync<ObjectDisposedException>(async () => await manager.GetFieldsInTypeAsync(Guid.NewGuid()));
        }
    }
}
